
先前我們已經將登入和註冊頁開發完,今天要來進行導覽列的開發。
未登入
登入後
根據wireframe,我們將首頁和導覽列分別建立,因為導覽列在之後的頁面都會存在,是需要共用的元件。
我們先在layout資料夾底下新增Header.js,並進行切版和路由設定(點擊右上角的Sign In 時要能跳轉到Login頁面)
import { Link } from "react-router-dom";
const Header = (props) => {
    return (
       <header className="w-screen h-14 bg-black flex justify-between items-center px-8">
        <Link to="/">
          <div className="flex justify-between items-center">
            {/* <img className="h-[24px]" src={logoImage} alt="logo" /> */}
            <h3 className="ml-4 text-white">BLOG DEV</h3>
          </div>
        </Link>
        <div className="text-gray-200">
        <Link to="/posts">
          <button className="px-8 py-2 hover:text-white">Posts</button>
        </Link>
        <Link to="/tags">
          <button className="px-8 py-2 hover:text-white">Tags</button>
        </Link> 
        <Link to="/login">
          <button className="px-8 py-2 hover:text-white">Contact</button>
        </Link>
        </div>
        <div className="flex items-center">
            <Link to="/login">
              <button className="text-gray-200 px-4 py-2 hover:text-white">
                Sign In
              </button>
            </Link>
        </div>
      </header>
  );
};
export default Header;
回到App.js加上Header.js的路由
//App.js
import Header from '../layout/Header;
import Login  from './pages/Login';
import Register from './pages/Register';
import './App.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
  {
    path: '/', 
    element: <Header/>,
  },
  {
    path: '/login', 
    element: <Login/>,
  },
  {
    path: '/register', 
    element: <Register/>,
  }
])
const App = () => {
  return (
      <RouterProvider router={router}/>
  );
}
export default App;
上面只製作了未登入時的導覽列,若使用者是有登入時,是需要顯示發布文章的按鈕和他的預設頭像,但我們要怎麼判斷使用者是否登入了呢?
這時就需要使用React的Context
React Context API是 React.js 提供的一個用於元件間共享值的機制。它可以讓你在不必透過 props 鏈(props chain)將值手動傳遞的情況下,將值共享給元件樹中的任何一個元件。React Context
因為react context有以上特性,所以我們就能透過它來紀錄使用者是否登入的值
在contexts資料夾底下建立AuthContext.js檔案:
//AuthContext.js
import React, { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
//自訂hook useAuth,用來回傳當前的Context值,,方便在其他元件中使用
export const useAuth = () => {
  return useContext(AuthContext);
};
//透過AuthContext.Provider提供一個context的值給它的子元件。
//{ children }是它的子元件。透過value prop將認證狀態(isLoggedIn)和改變狀態的函數(setIsLoggedIn)傳遞給所有的子元件。
export const AuthProvider = ({ children }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(!!localStorage.getItem('user'));
 
  
  return (
    <AuthContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
      {children}
    </AuthContext.Provider>
  );
};
先回到App.js,在原本的 <RouterProvider router={router}/>外加上<AuthProvider><AuthProvider/>,這樣我們才能存取到isLoggedIn和setIsLoggedIn。
import './App.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import Login  from './pages/Login';
import Register from './pages/Register';
import HomePage from './pages/Home';
const router = createBrowserRouter([
  { 
      path: '/', 
      element: <HomePage />
  },
  {
    path: '/login', 
    element: <Login/>,
  },
  {
    path: '/register', 
    element: <Register/>,
  }
])
const App = () => {
  return (
    <AuthProvider>
      <RouterProvider router={router}/>
    </AuthProvider>
  );
}
export default App;
到Login.js,我們需要在登入成功後將isLoggedIn設為true
//Login.js
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; //引入useAuth
import api from "../api/api";
import Card from "../components/Card";
import useInput from "../hooks/useInput";
const LoginPage = (props) => {
  const navigate = useNavigate();
  
  //透過useAuth取得setIsLoggedIn的方法
  const { setIsLoggedIn } = useAuth();
  (略...)
  const handleSubmit = (event) => {
    if (!formIsValid) return;
    event.preventDefault();
    const userData = {
      email,
      password,
    };
    api
      .post("/auth/login", userData)
      .then((result) => {
        localStorage.setItem("user", JSON.stringify(result));
        //登入成功後要將isLoggedIn設為true
        setIsLoggedIn(true); 
        navigate("/");
      })
      .catch((error) => {
        setErrorMsg('Login failed. Your email or password is incorrect.')
        console.error(error);
      });
  };
  return (
    <Card>
         (略...)
    </Card>
  );
};
export default LoginPage;
回到Header.js,引入useAuth並加上是否登入的判斷,若已登入則顯示頭像和建立文章的按鈕
import { Fragment } from "react";
import { Link, useNavigate } from "react-router-dom";
//引入useAuth
import { useAuth } from "../../contexts/AuthContext"; 
import avatar from "../../assets/avatar.jpg";
const Header = (props) => {
 return (
      <header className="w-screen h-14 bg-black flex justify-between items-center px-8">
        <Link to="/">
          <div className="flex justify-between items-center">
            {/* <img className="h-[24px]" src={logoImage} alt="logo" /> */}
            <h3 className="ml-4 text-white">BLOG DEV</h3>
          </div>
        </Link>
        <div className="text-gray-200">
        <Link to="/posts">
          <button className="px-8 py-2 hover:text-white">Posts</button>
        </Link>
        <Link to="/tags">
          <button className="px-8 py-2 hover:text-white">Tag</button>
        </Link> 
        </div>
        <div className="flex items-center">\
           {/* 判斷是否登入再顯示對應樣式 */}
           {isLoggedIn ? (
            <LoggedInComponents />
          ) : (
            <Link to="/login">
              <button className="text-gray-200 px-4 py-2 hover:text-white">
                Sign In
              </button>
            </Link>
          )}
        </div>
      </header>
   );
};
export default Header;
//登入後的樣式
const LoggedInComponents = () => {
  const navigate = useNavigate();
  const handleSignOut = () => {
    localStorage.removeItem("user");
    navigate("/login");
  };
  return (
    <Fragment>
      <Link to="/edit-post/new">
        <button className="flex items-center bg-violet-600 px-4 py-1 text-gray-100 hover:text-white hover:bg-violet-700 duration-150 mr-4 rounded">
          <AiOutlinePlus />
          <p className="ml-2">New Post</p>
        </button>
      </Link>
      <div className="w-[32px] h-[32px] rounded-full overflow-hidden bg-gray-500 text-white">
          User
      </div>
    </Fragment>
  );
};

當我們在點擊頭像時,可以叫出功能列,我們使用headless UI來製作Menu。
功能列裡面有
//Header.js
import { Fragment } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext"; 
import { AiOutlinePlus } from "react-icons/ai";
//引入headlessui
import { Menu, Transition } from "@headlessui/react";
import avatar from "../../assets/avatar.jpg";
const Header = (props) => {
   (略...)
}
export default Header;
//登入後的樣式
const LoggedInComponents = () => {
  const navigate = useNavigate();
  //登出
  const handleSignOut = () => {
    //清除localstorage的使用者資訊,並導回登入頁
    localStorage.removeItem("user");
    navigate("/login");
  };
  return (
    <Fragment>
      <Link to="/edit-post/new">
        <button className="flex items-center bg-violet-600 px-4 py-1 text-gray-100 hover:text-white hover:bg-violet-700 duration-150 mr-4 rounded">
          <AiOutlinePlus />
          <p className="ml-2">New Post</p>
        </button>
      </Link>
      <Menu as="div" className="relative inline-block text-left">
        <Menu.Button>
          <div className="w-[32px] h-[32px] rounded-full overflow-hidden bg-gray-500 text-white">
           User
          </div>
        </Menu.Button>
        <Transition
          as={Fragment}
          enter="transition ease-out duration-100"
          enterFrom="transform opacity-0 scale-95"
          enterTo="transform opacity-100 scale-100"
          leave="transition ease-in duration-75"
          leaveFrom="transform opacity-100 scale-100"
          leaveTo="transform opacity-0 scale-95"
        >
          <Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right bg-white shadow-lg focus:outline-none flex flex-col rounded">
            <Menu.Item as={Fragment}>
              <a
                href="/account-settings"
                className="p-2 text-gray-800 bg-white hover:bg-neutral-100 text-left"
              >
                Account settings
              </a>
            </Menu.Item>
            <Menu.Item as={Fragment}>
              <button
                className="p-2 text-gray-800 bg-white hover:bg-neutral-100 text-left"
                onClick={handleSignOut}
              >
                Sign out
              </button>
            </Menu.Item>
          </Menu.Items>
        </Transition>
      </Menu>
    </Fragment>
  );
};
Yihsuan 大你好,Header.js 裡的 <header> 的部分沒有 return,小提醒,感謝這系列文章,我繼續學習
感謝你的提醒!! 已修正